// 24_NormalMapping.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"


#include <iostream>
#include <map>

// GLEW
#define GLEW_STATIC
#include <gl/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// Shader
#include "../common/shaderwrapper.h"
// Camera
#include "../common/Camera.h"
// Model
#include "../common/Model.h"
// SOIL
#include <SOIL.h>
// GLM
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>


void error_callback (int error, const char* description);
void key_callback (GLFWwindow* window, int key, int scancode, int action, int mode);
void mouse_callback (GLFWwindow* window, double xpos, double ypos);
void scroll_callback (GLFWwindow* window, double xoffset, double yoffset);
void do_movement ();
GLuint loadTexture (GLchar* path, GLboolean alpha = GL_FALSE);
GLuint loadCubemap (std::vector<const GLchar*> faces);
void RenderQuad ();

const GLuint WIDTH = 800, HEIGHT = 600;
GLfloat lastX = 400, lastY = 300;

bool keys[1024];
GLfloat deltaTime = 0.0f;	// Time between current frame and last frame
GLfloat lastFrame = 0.0f;	// Time of last frame

							// Camera
Camera camera (glm::vec3 (0.0f, 0.0f, 3.0f));

// Global variables
bool normalMapping = true;

int main ()
{
	glfwInit ();
	glfwSetErrorCallback (error_callback);
	glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint (GLFW_RESIZABLE, GL_FALSE);

	GLFWwindow* window = glfwCreateWindow (WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
	if (window == nullptr) {
		std::cout << "Failed to create GLFW window." << std::endl;
		glfwTerminate ();
		return -1;
	}
	glfwMakeContextCurrent (window);
	glfwSetInputMode (window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

	glewExperimental = GL_TRUE;
	if (glewInit () != GLEW_OK) {
		std::cout << "Failed to initialize GLEW" << std::endl;
		glfwTerminate ();
		return -1;
	}

	glViewport (0, 0, WIDTH, HEIGHT);

	glfwSetKeyCallback (window, key_callback);
	glfwSetCursorPosCallback (window, mouse_callback);
	glfwSetScrollCallback (window, scroll_callback);

	glEnable (GL_DEPTH_TEST);

#pragma region "shader & textures & models "

	Shader shader ("normal_mapping.vtx", "normal_mapping.frag");

#pragma endregion


#pragma region "object_initialization"

	// Load textures
	GLuint diffuseMap = loadTexture ("../Resource/brickwall.jpg");
	GLuint normalMap = loadTexture ("../Resource/brickwall_normal.jpg");

	// Set texture units 
	shader.Use ();
	glUniform1i (glGetUniformLocation (shader.Program, "diffuseMap"), 0);
	glUniform1i (glGetUniformLocation (shader.Program, "normalMap"), 1);

	// Light position
	glm::vec3 lightPos (0.5f, 1.0f, 0.3f);

#pragma endregion

	while (!glfwWindowShouldClose (window)) {
		// Check and call events
		glfwPollEvents ();

		GLfloat currentFrame = glfwGetTime ();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		do_movement ();

		// Clear the colorbuffer
		glClearColor (0.1f, 0.1f, 0.1f, 1.0f);
		glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		// Configure view/projection matrices
		shader.Use ();
		glm::mat4 view = camera.GetViewMatrix ();
		glm::mat4 projection = glm::perspective (glm::radians (camera.zoom), (float)WIDTH / (float)HEIGHT, 0.1f, 100.0f);
		glUniformMatrix4fv (glGetUniformLocation (shader.Program, "view"), 1, GL_FALSE, glm::value_ptr (view));
		glUniformMatrix4fv (glGetUniformLocation (shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr (projection));
		// Render normal-mapped quad
		glm::mat4 model;
		model = glm::rotate (model, glm::radians((GLfloat)glfwGetTime () * - 10), glm::normalize (glm::vec3 (1.0, 0.0, 1.0))); // Rotates the quad to show normal mapping works in all directions
		glUniformMatrix4fv (glGetUniformLocation (shader.Program, "model"), 1, GL_FALSE, glm::value_ptr (model));
		glUniform3fv (glGetUniformLocation (shader.Program, "lightPos"), 1, &lightPos[0]);
		glUniform3fv (glGetUniformLocation (shader.Program, "viewPos"), 1, &camera.position[0]);
		glActiveTexture (GL_TEXTURE0);
		glBindTexture (GL_TEXTURE_2D, diffuseMap);
		glActiveTexture (GL_TEXTURE1);
		glBindTexture (GL_TEXTURE_2D, normalMap);
		RenderQuad ();

		// render light source (simply re-renders a smaller plane at the light's position for debugging/visualization)
		model = glm::mat4 ();
		model = glm::translate (model, lightPos);
		model = glm::scale (model, glm::vec3 (0.1f));
		glUniformMatrix4fv (glGetUniformLocation (shader.Program, "model"), 1, GL_FALSE, glm::value_ptr (model));
		RenderQuad ();

		glfwSwapBuffers (window);
	}

	// free VAOs,VBOs,TEXTUREs
	// ...

	glfwTerminate ();
	return 0;
}

// This function loads a texture from file. Note: texture loading functions like these are usually 
// managed by a 'Resource Manager' that manages all resources (like textures, models, audio). 
// For learning purposes we'll just define it as a utility function.
GLuint loadTexture (GLchar* path, GLboolean alpha)
{
	auto soil_load_opt = alpha ? SOIL_LOAD_RGBA : SOIL_LOAD_RGB;
	auto gl_texture_opt = alpha ? GL_RGBA : GL_RGB;

	//Generate texture ID and load texture data 
	GLuint textureID;
	glGenTextures (1, &textureID);
	int width, height;
	unsigned char* image = SOIL_load_image (path, &width, &height, 0, soil_load_opt);
	// Assign texture to ID
	glBindTexture (GL_TEXTURE_2D, textureID);
	glTexImage2D (GL_TEXTURE_2D, 0, gl_texture_opt, width, height, 0, gl_texture_opt, GL_UNSIGNED_BYTE, image);
	glGenerateMipmap (GL_TEXTURE_2D);

	// Parameters
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glBindTexture (GL_TEXTURE_2D, 0);
	SOIL_free_image_data (image);
	return textureID;
}

GLuint loadCubemap (std::vector<const GLchar*> faces) {
	GLuint textureID;
	glGenTextures (1, &textureID);
	glActiveTexture (GL_TEXTURE0);

	int width, height;
	unsigned char* image;

	glBindTexture (GL_TEXTURE_CUBE_MAP, textureID);
	for (GLuint i = 0; i < faces.size (); i++) {
		image = SOIL_load_image (faces[i], &width, &height, 0, SOIL_LOAD_AUTO);
		glTexImage2D (
			GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0,
			GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image
			);
		SOIL_free_image_data (image);
	}
	glTexParameteri (GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri (GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri (GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri (GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri (GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	glBindTexture (GL_TEXTURE_CUBE_MAP, 0);

	return textureID;
}


// RenderQuad() Renders a 1x1 quad in NDC, best used for framebuffer color targets
// and post-processing effects.
GLuint quadVAO = 0;
GLuint quadVBO;
void RenderQuad ()
{
	if (quadVAO == 0)
	{
		// positions
		glm::vec3 pos1 (-1.0, 1.0, 0.0);
		glm::vec3 pos2 (-1.0, -1.0, 0.0);
		glm::vec3 pos3 (1.0, -1.0, 0.0);
		glm::vec3 pos4 (1.0, 1.0, 0.0);
		// texture coordinates
		glm::vec2 uv1 (0.0, 1.0);
		glm::vec2 uv2 (0.0, 0.0);
		glm::vec2 uv3 (1.0, 0.0);
		glm::vec2 uv4 (1.0, 1.0);
		// normal vector
		glm::vec3 nm (0.0, 0.0, 1.0);

		// calculate tangent/bitangent vectors of both triangles
		glm::vec3 tangent1, bitangent1;
		glm::vec3 tangent2, bitangent2;
		// - triangle 1
		glm::vec3 edge1 = pos2 - pos1;
		glm::vec3 edge2 = pos3 - pos1;
		glm::vec2 deltaUV1 = uv2 - uv1;
		glm::vec2 deltaUV2 = uv3 - uv1;

		GLfloat f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

		tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
		tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
		tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
		tangent1 = glm::normalize (tangent1);

		bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
		bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
		bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
		bitangent1 = glm::normalize (bitangent1);

		// - triangle 2
		edge1 = pos3 - pos1;
		edge2 = pos4 - pos1;
		deltaUV1 = uv3 - uv1;
		deltaUV2 = uv4 - uv1;

		f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

		tangent2.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
		tangent2.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
		tangent2.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
		tangent2 = glm::normalize (tangent2);


		bitangent2.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
		bitangent2.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
		bitangent2.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
		bitangent2 = glm::normalize (bitangent2);


		GLfloat quadVertices[] = {
			// Positions            // normal         // TexCoords  // Tangent                          // Bitangent
			pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
			pos2.x, pos2.y, pos2.z, nm.x, nm.y, nm.z, uv2.x, uv2.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
			pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,

			pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
			pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
			pos4.x, pos4.y, pos4.z, nm.x, nm.y, nm.z, uv4.x, uv4.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z
		};
		// Setup plane VAO
		glGenVertexArrays (1, &quadVAO);
		glGenBuffers (1, &quadVBO);
		glBindVertexArray (quadVAO);
		glBindBuffer (GL_ARRAY_BUFFER, quadVBO);
		glBufferData (GL_ARRAY_BUFFER, sizeof (quadVertices), &quadVertices, GL_STATIC_DRAW);
		glEnableVertexAttribArray (0);
		glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 14 * sizeof (GLfloat), (GLvoid*)0);
		glEnableVertexAttribArray (1);
		glVertexAttribPointer (1, 3, GL_FLOAT, GL_FALSE, 14 * sizeof (GLfloat), (GLvoid*)(3 * sizeof (GLfloat)));
		glEnableVertexAttribArray (2);
		glVertexAttribPointer (2, 2, GL_FLOAT, GL_FALSE, 14 * sizeof (GLfloat), (GLvoid*)(6 * sizeof (GLfloat)));
		glEnableVertexAttribArray (3);
		glVertexAttribPointer (3, 3, GL_FLOAT, GL_FALSE, 14 * sizeof (GLfloat), (GLvoid*)(8 * sizeof (GLfloat)));
		glEnableVertexAttribArray (4);
		glVertexAttribPointer (4, 3, GL_FLOAT, GL_FALSE, 14 * sizeof (GLfloat), (GLvoid*)(11 * sizeof (GLfloat)));
	}
	glBindVertexArray (quadVAO);
	glDrawArrays (GL_TRIANGLES, 0, 6);
	glBindVertexArray (0);
}

// Generates a texture that is suited for attachments to a framebuffer
GLuint generateAttachmentTexture (GLboolean depth, GLboolean stencil)
{
	// What enum to use?
	GLenum attachment_type;
	if (!depth && !stencil) {
		attachment_type = GL_RGB;
	}
	else if (depth && !stencil) {
		attachment_type = GL_DEPTH_COMPONENT;
	}
	else if (!depth && stencil) {
		attachment_type = GL_STENCIL_INDEX;
	}

	//Generate texture ID and load texture data 
	GLuint textureID;
	glGenTextures (1, &textureID);
	glBindTexture (GL_TEXTURE_2D, textureID);
	if (!depth && !stencil) {
		glTexImage2D (GL_TEXTURE_2D, 0, attachment_type, WIDTH, HEIGHT, 0, attachment_type, GL_UNSIGNED_BYTE, NULL);
	}
	else { // Using both a stencil and depth test, needs special format arguments
		glTexImage2D (GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL);
	}
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glBindTexture (GL_TEXTURE_2D, 0);

	return textureID;
}

#pragma region "User input"

void error_callback (int error, const char* description)
{
	std::cout << (description) << std::endl;
}

bool keysPressed[1024];
void key_callback (GLFWwindow* window, int key, int scancode, int action, int mode)
{
	// When a user presses the escape key, we set the WindowShouldClose property to true,
	// closing the application

	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
		glfwSetWindowShouldClose (window, GL_TRUE);
	}

	if (key >= 0 && key <= 1024) {
		if (action == GLFW_PRESS) {
			keys[key] = true;
		}
		else if (action == GLFW_RELEASE) {
			keys[key] = false;
			keysPressed[key] = false;
		}
	}
}

void do_movement () {
	// Camera controls
	if (keys[GLFW_KEY_W]) {
		camera.ProcessKeyboard (Camera::FORWARD, deltaTime);
	}
	if (keys[GLFW_KEY_S]) {
		camera.ProcessKeyboard (Camera::BACKWARD, deltaTime);
	}
	if (keys[GLFW_KEY_A]) {
		camera.ProcessKeyboard (Camera::LEFT, deltaTime);
	}
	if (keys[GLFW_KEY_D]) {
		camera.ProcessKeyboard (Camera::RIGHT, deltaTime);
	}
	if (keys[GLFW_KEY_4]) {
		// wireframe: GL_LINE
		// GL_FILL
		glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);
	}
	if (keys[GLFW_KEY_5]) {
		glPolygonMode (GL_FRONT_AND_BACK, GL_FILL);
	}
	if (keys[GLFW_KEY_SPACE] && !keysPressed[GLFW_KEY_SPACE]) {
		normalMapping = !normalMapping;
		keysPressed[GLFW_KEY_SPACE] = true;
	}
}

bool firstMove = true;
void mouse_callback (GLFWwindow* window, double xpos, double ypos) {
	if (firstMove) {
		lastX = xpos;
		lastY = ypos;
		firstMove = false;
	}

	GLfloat xoffset = xpos - lastX;
	GLfloat yoffset = lastY - ypos;	// Reversed since y-coordinates range from bottom to top
	lastX = xpos;
	lastY = ypos;

	camera.ProcessMouseMovement (xoffset, yoffset);
}

void scroll_callback (GLFWwindow* window, double xoffset, double yoffset) {
	camera.ProcessMouseScroll (yoffset);
}

#pragma endregion

